读书笔记-Redis 单机数据库的实现

Redis:单机数据库的实现

1. 数据库

通过 SELECT num 命令可以切换客户端使用的数据库, 即让客户端 db 指针指向服务端的某个 db 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct redisServer {

// 一个数组, 保存着服务器中的所有数据库
redisDb *db;

// 服务器的数据库数量: 默认16个
int dbnum;

// AOF 缓冲区: 记录写操作
sds aof_buf;

// AOF 重写缓冲区: 记录在 AOF 重写机制期间的所有写操作, 用于完成数据一致性
sds aof_rewrite_buf;

// 一个链表, 保存了所有客户端状态
list *clients;
}

struct redisClient {

// 记录客户端当前正在使用的数据库
redisDb *db;

// 输入缓冲区: 用于保存客户端发送的命令请求
sds querybuf;

// 命令的实现函数
struct redisCommand *cmd;
}

Redis 是一个键值对(key-value pair)数据库服务器, 服务器中的每个数据库都由一个 redisDb 结构表示

1
2
3
4
5
6
7
8
struct redisDb {

// 数据库键空间, 保存着数据库中的所有键值对
dict *dict;

// 过期字典, 保存着键的过期时间, EXPIRE 实际上是操作该键值
dict *expires;
}

主要由 dict 和 expires 两个字典构成, 其中 dict 字典负责保存键值对, 而 expires 字典则负责保存键的过期时间

dict 内容是 < StringObject - 对象 > 的键值对, 当使用 Redis 命令对数据库进行读写时, 服务器不仅会对键空间执行指定的读写操作, 还会执行一些额外的维护操作, 其中包括:

  1. 在读取一个键之后, 服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或不命中(miss)次数, 这两个值可以在 INFO stats 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看

  2. 在读取一个键之后, 服务器会更新键的 LRU 时间, 用于计算键的闲置时间, 使用 OBJECT idletime 命令可以查看 key 的闲置时间

  3. 如果服务器在读取一个键时发现该键已经过期, 那么服务器会先删除这个过期键, 然后才执行余下的其他操作

  4. 如果有客户端使用 WATCH 命令监视了某个键, 那么服务器在对被监视的键进行修改之后, 会将这个键标记为 dirty, 从而让事务程序注意到这个键已经被修改过

  5. 服务器每次修改一个件之后, 都会对脏键计数器的值增 1, 这个计数器会触发服务器的持久化以及复制操作

  6. 如果服务器开启了数据库通知功能, 那么在对键进行修改之后, 服务器将按配置发送相应的数据库通知

当键过期时, Redis 提供了三种删除策略

  1. (主动)定时删除: 在设置键的过期时间的同时, 创建一个定时器(timer), 让定时器在键的过期时间来临时, 立即执行对键的删除操作, 省内存, 费CPU

  2. (Redis使用)(被动)惰性删除: 放任键过期不管, 当获取的时候, 过期就删除, 未过期就返回, 省CPU, 费内存(expireIfNeeded)

  3. (Redis使用)(主动)定期删除: 每隔一段时间, 程序就对数据库进行一次检查, 删除里面的过期键(算法), 折中(serverCron - activeExpireCycle)

AOF 和 RDB 是如何删除过期键的

  1. 执行 SAVE 命令或者 BGSAVE 命令所产生的新 RDB 文件不会包含已经过期的键

  2. 执行 BGREWRITEAOF 命令所产生的重写 AOF 文件不会包含已经过期的键

  3. 当一个过期键被删除之后, 服务器会追加一条 DEL 命令到现有 AOF 文件的末尾, 显式的删除过期键

  4. 当主服务器删除一个过期键之后, 它会向所有从服务器发送一条 DEL 命令, 显式的删除过期键

  5. 从服务器即使发现过期键也不会自作主张的删除它, 而是等待主节点发来 DEL 命令, 从而保证主从服务器数据一致性

2. RDB 持久化

RDB 持久化操作, 即执行 SAVE/BGSAVE 命令, 是由配置文件中的 save 选项生效的, 由周期性操作函数 serverCron 每个 100ms 检查是否满足持久化条件

1
2
3
4
// default
save 900 1 900秒内对数据库至少1次修改
save 300 10 300秒内对数据库至少10次修改
save 60 10000 60秒内对数据库至少10000次修改

使用 od 命令工具可以查看 RDB 内容

  • SAVE 命令由服务器进程直接执行保存操作, 会阻塞服务器

  • BGSAVE 命令由子进行执行保存操作, 不会阻塞服务器执行, 但是会拒绝再次服务器进行再次接收的 SAVE, BGSAVE, BGREWRITEAOF 命令防止竞争

  • 服务器状态中会保存所有用 save 选项设置的保存条件, 当任意一个保存条件被满足时, 服务器会自动执行 BGSAVE 命令

  • RDB 是一个经过压缩的二进制文件, 有多个部分组成

  • 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们

3. AOF 持久化
  1. AOF 持久化是通过保存 Redis 服务器所执行的写命令(SET、SADD、RPUSH)来记录数据库状态的, 这个保存的过程是由 serverCron 函数中的 flushAppendOnlyFile 来完成的, 也是由配置文件中的 appendfsync 选项来设置的

  2. 随着服务器时间的运行, AOF 中记录的写操作越来越多, 文件体积也越来越大, 使用 AOF 文件来进行数据还原所需的时间也就越多, 因此 AOF 文件重写机制: 通过读取服务器当前的数据库状态, 然后用一条命令去记录键值对, 代替之前记录这个键值对的多条命令

  3. AOF 重写机制采用后台子进程的方式来实现, 虽然不阻塞服务器继续处理命令请求, 但随之而来带来了数据同步不一致的问题, 因此 Redis 服务器设置了一个 AOF 重写缓存区, 每次子进程重写完成时, 服务器进程将 AOF 缓冲区的记录追加到新的 AOF 文件末尾, 这也是 BGREWRITEAOF 命令的实现原理

4. 事件
  • Redis 服务器是一个事件驱动程序, 服务器处理的事件分为时间事件和文件事件两类

  • 文件事件处理器是基于 Reactor 模式实现的网络通信程序

  • 文件事件是对套接字操作的抽象: 每次套接字变为可应答(acceptable), 可写(writable)或者可读(readable)时, 相应的文件事件就会产生

  • 文件事件分为 AE_READABLE 和 AE_WRITABLE 两类事件

  • 时间事件分为定时事件和周期性事件

  • 服务器一般情况下只执行 serverCron 函数的一个时间事件, 并且这个事件是周期性事件

  • 时间事件的实际处理时间通常比设定的到达时间晚一些

5. 客户端

通过使用由 I/O 多路复用技术实现的文件处理器, Redis 服务器使用单线程单进程的方式来处理命令请求, 并与多个客户端进行网络通信

1
2
127.0.0.1:6379> CLIENT list
id=5 addr=127.0.0.1:58793 fd=8 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

通过客户端的标志属性 flags 可以查明客户端的角色, 以及目前客户端所处的状态, 例如

1
2
3
4
5
6
7
8
9
# 客户端是一个主服务器
REDIS_MASTER
# 客户端正在被列表命令阻塞
REDIS_BLOCKED
# 客户端正在执行事务, 但事务的安全性已被破坏
REDIS_MULTI | REDIS_DIRTY_CAS
# 客户端是一个从服务器, 并且版本低于 Redis 2.8
REDIS_SLAVE | REDIS_PRE_PSYNC
...
  • 服务器状态结构使用 clients 链表连接多个客户端状态, 新添加的客户端状态会被放到链表的末尾

  • 客户端状态的 flags 属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态

  • 输入缓冲区记录了客户端发送的命令请求, 这个缓冲区的大小不能超过 1GB

  • 命令参数和参数个数会被记录在客户端状态的 argv 和 argc 属性里面, 而 cmd 属性则记录对了客户端要执行命令是实现函数

  • 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用, 其中固定大小缓冲区的最大大小为 16KB, 而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制

  • 输出缓冲区限制值有两种, 如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭; 除此之外, 如果客户端在一定时间内, 一直超过服务器设置的软性限制, 那么客户端也会被关闭

  • 当一个客户端通过网络连接上服务器时, 服务器会为这个客户端创建相应的客户端状态, 网络连接关闭、发送了不合协议格式的命令请求、成为 CLIENT KILL 命令的目标、空转时间超时、输出缓冲区的大小超出限制, 以上这些原因都会造客户端被关闭

  • 处理 LUA 脚本的伪客户端在服务器初始化时创建, 这个客户端会一直存在, 直到服务器关闭

  • 载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建, 载入工作完毕之后关闭

6. 服务器
  • 一个命令请求从发送到完成主要包括以下几个步骤: 1) 客户端将命令请求发送给服务器; 2) 服务器读取命令请求; 3) 命令执行器根据参数查找命令的实现函数; 4) 服务器将命令回复给客户端

  • serverCron 函数默认每隔 100ms 执行一次, 它的主要工作包括更新服务器状态信息, 处理服务器接收的 SIGTERM 信号, 管理客户端资源和数据库状态, 检查并执行持久化操作

  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤: 1) 初始化服务器状态; 2) 载入服务器配置; 3) 初始化服务器数据结构; 4) 还原数据库状态; 5) 执行事件循环

如需转载,请注明出处